Skip to content

feat(spec): object-kanban props declare limit, the row cap objectui already implements - #16562

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-16503-object-kanban-limit-prop
Sep 7, 2026
Merged

feat(spec): object-kanban props declare limit, the row cap objectui already implements#16562
huangyiirene merged 2 commits into
mainfrom
claude/issue-16503-object-kanban-limit-prop

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #16503

Clause-②: yes

What

ComponentPropsMap['object-kanban'] (packages/spec/src/ui/component.zod.ts) gains one optional authorable key:

limit: z.number().int().positive().optional()

describe: Maximum number of records loaded onto the board (row cap); lowered to the query's top-level $top (renderer default 100). The component-level dataSource.limit wins when both are set; a bound view's pagination.pageSize fills it only when unset — the card's sentence, plus the precedence the renderer applies (the sibling limit describes in this map, element:record_picker and record:related_list, name their renderer default and binding precedence the same way).

Widening a published accept set: Clause-② yes, @objectstack/spec minor changeset (.changeset/object-kanban-limit-row-cap.md), needs:contract-review on this PR and on #16503. The PR stays draft; the ready flip, auto-merge and queueing are the PM's after the at-tier review.

Premise, re-measured at the objectui pin

Readings pinned to this repo's .objectui-sha = a472b07167a39e55491109e864bb5a54027dcfbd (never objectui's moving branch):

  • packages/plugin-kanban/src/ObjectKanban.tsx:262-266 — the board's one query is dataSource.find(objectName, { $filter: schema.filter, $top: schema.limit ?? DEFAULT_KANBAN_LIMIT }); DEFAULT_KANBAN_LIMIT = 100 at :71; a real top-level $top since objectui#4025.
  • packages/plugin-kanban/src/index.tsx:395-398OBJECT_KANBAN_DATA_SOURCE = { filter: true, limit: 'limit' }.
  • packages/plugin-kanban/src/types.ts:134KanbanSchema.limit?: number, the type ObjectKanban.tsx:143 reads schema through. One correction to the card's spelling: at the pin, @object-ui/types' ObjectKanbanSchema (zod mirror types/src/zod/objectql.zod.ts:875-889, interface types/src/objectql.ts:2693) carries no limit; the declaring face is KanbanSchema in the plugin package. Four faces still agree; the ruling's substance is unchanged.
  • content/docs/plugins/plugin-kanban.mdx:108, 153-172 — the Properties row, the limit: 250 snippet (default 100), and the note separating the board's limit from a column's WIP limit.
  • Before this PR, at BASE f48f3f1b21fe207c39d94e49609ac49e19635e3b: safeParse({ objectName: 'x', limit: 250 }) yields unrecognized_keys: ['limit'] — the same verdict as bogusProp. Leg B below reproduces it.

The judgement the card opened: limit, not pagination.pageSize

Decided for the default and not proposed as a change, because at the pin the alternative is not a competing spelling but an upstream source lowered into limit: packages/react/src/element-data-source/ElementDataSourceGate.tsx:229-241 (readLimit / writeLimit, keyed by ElementDataSourceLimitKey, whose members are limit, pageSize and pagination.pageSize) writes the binding's dataSource.limit — or, only when the component authored none, the bound view's pagination.pageSize — onto the key the block's mapping names, and kanban's mapping names limit. The board has no pagination read point, so declaring pagination.pageSize on object-kanban would name a key the renderer ignores — the accepted-and-dropped defect the card's ruling exists to remove. The docblock on the key records this.

Generated footprint (tracked vs gitignored)

Verification — all on fb654377e2, the final commit; the tree did not change afterwards

Pins (packages/spec/src/ui/component.test.ts, 4 new cases): accept { objectName: 'x', limit: 250 } and carry the value through; refuse bogusProp with unrecognized_keys: ['bogusProp'] (the half that proves the object stayed strict); refuse 0 / -1 / 1.5 / '250' at the value (issue path ['limit'], no unrecognized_keys); the describe names $top, row cap and dataSource.limit.

  • vitest run src/ui/component.test.ts: 251 passed; with --reporter=verbose -t 16503: 4 of 4 passed.
  • Reverse verification (leg B): component.zod.ts reverted to BASE (blob a549b824…, equal to the BASE blob, not equal to HEAD 3d939c5e…), same test file: 3 failed, 1 passed, 247 skipped — the accept pin, the value pins and the describe pin red; the bogusProp control green, as designed. Restored with git checkout HEAD --; blob back to 3d939c5e…, git diff HEAD empty, git status --porcelain empty.
  • Ablation of the checked-in baseline (leg A, no rebuild — a rebuild regenerates authorable-surface/ and would come back falsely green): the ui/ObjectKanbanProps:limit row deleted from authorable-surface/ui.json (anchor count 1 to 0; blob eb3c99d7… not equal to HEAD 1b290888…), then check:authorable-surface: exit 1, "authorable-surface/ is out of date (1 key(s) not recorded). + ui/ObjectKanbanProps:limit". Restored with git checkout HEAD --; blob back to 1b290888…, anchor count 1.
  • Package checks: pnpm --filter @objectstack/spec typecheck exit 0 (check:test-typecheck included). vitest run over the 11 spec test files (of 425) that name the props map, object-kanban, component.zod or a snapshot: 11 files, 732 tests passed. pnpm --filter @objectstack/lint test: 100 files, 3486 tests passed — lint is the in-repo consumer of ComponentPropsMap (validate-component-props.ts, validate-react-page-props).
  • Gates: node scripts/pm/dispatch-gates.mjs --ran: 98 derived, 97 run, 1 unruncheck:react-declaration-parity, which cannot run in this repo (its right-hand side is objectui's sdui.manifest.json; NOT MEASURED, not a pass). Green, verdict lines read from each gate's own output: check:generated (15 artifacts current after gen:docs), check:api-surface, check:authorable-surface, check:docs, check:objectui-pin-citations (12 asserting citations match a472b0716), check:duration-unit-keys, check:skill-examples (257 blocks across 3 surfaces, after building @objectstack/client-react), check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:docs-transcript-drift, the changeset gates (check-adr-0087-registration: no declared-breaking changeset; check-empty-changeset; check-changeset-no-major), check:liveness, check:strictness-ledger, check:yaml-examples (18 component nodes judged against their props schema), and the rest of the derived list. NOT MEASURED (exit 3, prerequisite — they need the full pnpm build closure): check:type-check-debt, check:dual-build-cjs-loads — CI's.
  • Declared narrowing: turbo ls --affected lists every package downstream of @objectstack/spec. Locally: the spec test files that name the props map, object-kanban, component.zod or a snapshot, plus the whole @objectstack/lint suite. The remaining downstream suites are CI's. The showcase consumer test examples/app-showcase/test/my-work-visibility.test.ts queued out twice on the shared verify lock (exit 99, NOT MEASURED); it names ComponentPropsMap only in a comment, and no test in the repo asserts that limit is refused on object-kanban.

What this PR does not do

  • objectui#8172 stays open: the registry declaration that publishes the key on objectui's side is that card's; this PR notifies it.
  • No api-surface shard, no error-code ledger, no conversion entry — a widened accept set is non-breaking and needs no ADR-0087 disposition.

Generated by Claude Code

… already reads

`ComponentPropsMap['object-kanban']` is strict and refused `limit` by name
while objectui's plugin-kanban reads `schema.limit` as the query's `$top`,
`OBJECT_KANBAN_DATA_SOURCE` maps `limit: 'limit'`, `KanbanSchema` declares
`limit?: number` and the plugin docs teach `limit: 250`. Declare the key
(`z.number().int().positive().optional()`), pin both directions (the
documented shape parses; an undeclared sibling is still refused), and add
the minor changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…ect-kanban `limit`

`pnpm --filter @objectstack/spec build` (gen:schema) adds the
`ui/ObjectKanbanProps:limit` row to `authorable-surface/ui.json`;
`check:generated` proved only `content/docs/references/**` stale and
`gen:docs` regenerated it. No `api-surface/` shard moves — a key on an
existing props map adds no export.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
@github-actions github-actions Bot added the size/m label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 130 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5e7ff5f6c68b43783895e1252716f0bf80f41929packageMentionDocs.

Which tree this was computed on

This run read content/docs from 117b4a100c2d33c960574a1793fd3ba110bcad34 — the merge of head fb654377e285aa973ef0d3931d975da9eca3cba2 into base 5e7ff5f6c68b43783895e1252716f0bf80f41929, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 117b4a100c2d33c960574a1793fd3ba110bcad34 && git checkout 117b4a100c2d33c960574a1793fd3ba110bcad34
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5e7ff5f6c68b43783895e1252716f0bf80f41929 fb654377e285aa973ef0d3931d975da9eca3cba2 && git checkout -B drift-repro 5e7ff5f6c68b43783895e1252716f0bf80f41929 && git merge --no-ff fb654377e285aa973ef0d3931d975da9eca3cba2

node scripts/docs-audit/affected-docs.mjs --json 5e7ff5f6c68b43783895e1252716f0bf80f41929

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Copy link
Copy Markdown
Collaborator Author

Contract review at CONTRACT_REVIEW_TIER (claude-fable-5-1) — PASS, no blocking findings

Anchors: origin/main 90e7e6de11, head fb654377e2, merge-base f48f3f1b21, .objectui-sha a472b07167 on both sides. Every objectui reading is git show <pin>:path, never a branch. Measurements in a detached worktree at head, --offline --ignore-scripts, no spec build before any ablation — which is the discipline that keeps the authorable-surface leg from going falsely green.

The card's open judgement was closed correctly

The reviewer re-derived the whole chain at the pin rather than accepting the author's account of it: ElementDataSourceGate.tsx:236-241 writes into the key the block's mapping names; readLimit/writeLimit are keyed by 'limit' | 'pageSize' | 'pagination.pageSize'; kanban's mapping is { filter: true, limit: 'limit' }; and plugin-kanban/src has no code read of pagination/pageSize at all — only three comments, with the real limit reads at ObjectKanban.tsx:264, 291 lit as the control.

So declaring pagination.pageSize on the props would have been accepted-and-dropped: a bound view's pagination.pageSize already flows into limit today. The author judged the ruling's conditional proposal and recorded why they declined it. That is the right answer, reached the right way.

One point in the diff that is easy to miss and is load-bearing

.optional() rather than .default(100) is not cosmetic. A schema default would materialize limit on every parsed board and defeat the gate's readLimit(base) === undefined branch — so a bound view's pageSize would then never fill it. The choice is what keeps the precedence table working.

Bounds are right too: positive() excludes 0, and 0 is not "unlimited" — the adapter forwards $top on !== undefined, the wire schema allows min(0) while the engine requires min(1), so 0 is either zero rows or a refusal. No upper bound exists on any $top path (control: 15 non-test consumer files; the only MAX_*LIMIT is the MCP tool surface), so there is none to state.

⚠️ Correction to this PR's own body — it under-reports a gate

The body says check:react-declaration-parity "cannot run in this repo." That is false, and the correction runs in the safe direction: the gate runs and it passes.

sdui.manifest.json is tracked at the repo root (blob 78f870e4…, identical on main and this head, landed by #13446), and CI's lint job runs the gate against it on every PR (.github/workflows/lint.yml:5645). The reviewer ran exactly that invocation: object-kanban: 2 declared by both, 12 spec-only, 0 registry-only"no new DECLARATION divergence vs accepted baseline", exit 0. It cannot regress structurally either: the baseline ratchets only registryOnly, and a spec-side addition can only ever be spec-only, which is soft.

This matters because the card itself named that gate as the risk of this change. It was the right gate to worry about, and it passes. Treat this comment as superseding that sentence in the body — I am not rewriting the author's report to hide the error, but the record should not stand uncorrected.

Why the author got it wrong is not their fault, and it is now a card. The script's own header (scripts/check-react-blocks-declaration-parity.ts:80-89) still says "This repository carries no manifest to fall back on" — stale since #13446 landed the manifest. The author read the script's header and believed it. Filed separately; it is a comment-only fix and explicitly not this PR's to carry.

Two remaining advisories, neither blocking

  • A2 (low) — the docblock/changeset say the docs section teaches limit with a typed snippet. At the pin, plugin-kanban.mdx:157-163 is const board = { in a ```tsx fence, untyped. objectui#8172 quoted the typed form but was reading objectui's later main (9bfd618), not the pin. The fourth face is the docs section either way.
  • A3 — the lint consumer suite was NOT MEASURED by the reviewer (needs the full dist closure) and is correctly labelled so rather than assumed green. Structurally decoupled: validate-component-props.test.ts places object-kanban only at :525 with { objectName, groupField }, and all nine limit mentions are on other blocks. CI's to confirm.

The author's own NOT MEASURED set was checked and is honest: the exit 99 on my-work-visibility.test.ts was correctly refused as a reading, and the reviewer confirmed nothing load-bearing hides there (no kanban/limit token in its 193 lines, with the ComponentPropsMap grep lit as control on the same file).

Scope and CI

Five files, zero under api-surface/ — and structurally none could be: the shard lists ObjectKanbanProps (type) / ObjectKanbanPropsSchema (const) by export name only, with the existing member coverImageField absent from every shard as the control. gen:schema left porcelain 0. No scope creep: the schema block differs from base only by the key, its docblock, and three docblock lines.

37 check runs, all 37 read. Three still in progress at this writing — Test Core (5/6), Test Core (6/6) and Lint & Repo Gates; everything else success or skipped. Not arming until those three land, Lint & Repo Gates in particular: earlier today I armed a different PR on a partial reading of this exact listing and it had a real failure sitting on page 2.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Armed — and why advisory A2 did not hold this PR, when a similar-looking one is holding #16563

All 38 check runs read (37 earlier plus a Test Core rollup), every one success or skipped; the three that were outstanding — Test Core (5/6), Test Core (6/6), Lint & Repo Gates — all landed green. Five files changed, none on the governed surface. needs:contract-review struck, ready flipped, auto-merge on.

A2 is a real inaccuracy and I am not hiding it. The changeset and the limit docblock both say the docs page "teaches it with a typed snippet (limit: 250)". At the pin this repo builds against (.objectui-sha = a472b0716), content/docs/plugins/plugin-kanban.mdx:157-163 is const board = { inside a ```tsx fence — no type annotation. The typed form objectui#8172 quoted exists on objectui's later main (9bfd618), not at the pin. So the word "typed" is false against the sha the docblock itself cites.

I am landing it anyway, and the distinction from #16563 is one I want on the record rather than left to look inconsistent:

#16563's held claim this PR's A2
what it asserts cancelRun answers true only when this call consumed the suspension a docs page's snippet carries a type annotation
kind a behavioural guarantee of the contract a provenance detail in an evidence citation
who builds on it #13953's door, directly — a caller reading "I was the one who cancelled" out of true would act twice nobody; no code depends on it
if wrong a real downstream defect ships an auditor re-measures, which is what they would do regardless

The substance of this citation is true: the page does teach limit: 250 and does carry a Properties row, so the fourth face is genuinely there. One adjective is wrong about its formatting. That does not warrant a full CI cycle to correct, and it self-heals when the objectui pin advances past 9bfd618.

⚠️ For whoever next edits ObjectKanbanPropsSchema's docblock: drop the word "typed" if the pin has not moved by then. Noted here rather than as a card, because a card for one adjective is worse noise than the adjective.

The error's shape is worth naming though, because it recurs: the claim was read off a different sha than the one the docblock anchors to. That is the same family as several corrections in this lane today — a reading taken against a moving ref and then recorded as if it were taken against the pin.


Generated by Claude Code

@huangyiirene
huangyiirene added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit de0bcdd Sep 7, 2026
43 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-16503-object-kanban-limit-prop branch September 7, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

2 participants